﻿namespace NTLib.Pipe.Network.Connectors
{
    using System.Threading;
    using System.Threading.Tasks;
    using NTLib.Core.Threading;
    using System.Net.Sockets;
    using System.IO;
    using NTLib.Pipe.Network.Exceptions;
    using NTLib.Pipe.Network.Descriptors;
    using Core.Attributes;
    using System.Net;
    using Core.ScopeLock;
    using System;
    using System.Diagnostics;
    using Streaming.Descriptors.Packages;
    using Streaming.Containers;
    using Streaming.Packages;

    /// <summary>
    /// Send data through the network using the TCP/IP protocol.
    /// </summary>
    /// <seealso cref="NTLib.Pipe.Network.Connectors.BaseConnector" />
    [Guid("baad90d7-ebf3-4442-aff5-c7e9357d256e")]
    public class TcpConnector : BaseConnector
    {
        #region Fields

        private TcpClient _client;
        private SemaphoreSlim _resourceAccess;

        #endregion

        #region CTOR

        /// <summary>
        /// Initializes a new instance of the <see cref="TcpConnector"/> class.
        /// </summary>
        public TcpConnector()
        {
            this._resourceAccess = new SemaphoreSlim(1);
        }


        #endregion

        #region Properties

        /// <summary>
        /// Gets a value indicating whether this instance can pop item.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance can pop item; otherwise, <c>false</c>.
        /// </value>
        public override bool CanPopItem
        {
            get { return true; }
        }

        /// <summary>
        /// Gets the value indicating that the current pipe allow the user to push items through it.
        /// </summary>
        /// <value>
        /// <c>true</c> if this instance can push item; otherwise, <c>false</c>.
        /// </value>
        public override bool CanPushItem
        {
            get { return true; }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Asynchroniously send data
        /// </summary>
        public override async Task OnSendAsync(IPackage pkg, CancellationToken token = default(CancellationToken))
        {
            NetworkStream stream = null;

            using (this._resourceAccess.ScopeWait(token))
            {
                CheckClientAvailable();
                stream = this._client.GetStream();
            }

            try
            {
                byte[] bytes = new byte[pkg.Stream.Length];

                await pkg.Stream.ReadAsync(bytes, 0, (int)pkg.Stream.Length, token);
                await stream.WriteAsync(bytes, 0, bytes.Length, token);
            }
            catch (Exception ex)
            {
                if (ex is ObjectDisposedException)
                {
                    this.Connected = ConnectionStatus.Disconnected;
                }

                throw;
            }
        }

        /// <summary>
        /// Closes the connector.
        /// </summary>
        protected override async Task OnCloseAsync(CancellationToken token = default(CancellationToken))
        {
            using (this._resourceAccess.ScopeWait(token))
            {
                CheckClientAvailable();
                var client = this._client;
                if (this._client != null)
                {
                    this._client = null;
                    client.Close();
                    while (client.Client != null && client.Connected)
                    {
                        await Task.Delay(5);
                    }
                    client.Dispose();
                    this.Connected = ConnectionStatus.Disconnected;
                }
            }
        }

        /// <summary>
        /// Connects the connector.
        /// </summary>
        /// <param name="token"></param>
        protected override async Task OnConnectAsync(CancellationToken token = default(CancellationToken))
        {
            using (this._resourceAccess.ScopeWait(token))
            {
                if (this._client == null)
                {
                    if (this.IsHost)
                    {
                        TcpListener listener = new TcpListener(IPAddress.Any, this.Address.Uri.Port);
                        using (ScopeLockItem<TcpListener> listenerLock = new ScopeLockItem<TcpListener>(listener, (l) => l.Stop()))
                        {
                            listener.Start();
                            //Debug.WriteLine("Listening for client");
                            var listenerTask = listener.AcceptTcpClientAsync();
                            this.Connected = ConnectionStatus.Waiting;
                            await Task.Run(() => listenerTask.Wait(token));
                            //Debug.WriteLine("Client is here");
                            this._client = listenerTask.Result;
                            this.Connected = (this._client.Connected) ? ConnectionStatus.Connected : ConnectionStatus.Disconnected;
                            //Debug.WriteLine("Client Connected " + this.Connected);
                            //this._client = listenerTask.Result;
                        }
                    }
                    else
                    {
                        this._client = new TcpClient();
                        //Debug.WriteLine("Connecting");
                        var task = this._client.ConnectAsync(this.Address.Uri.Host, this.Address.Uri.Port);
                        await Task.Run(() => task.Wait(token));
                        this.Connected = (this._client.Connected) ? ConnectionStatus.Connected : ConnectionStatus.Disconnected;
                        //Debug.WriteLine("Connected : " + this.Connected);

                    }
                }
            }
        }

        /// <summary>
        /// Asynchroniously read data
        /// </summary>
        protected override async Task<IPackage> OnReadAsync(IPackageDescriptor desc, CancellationToken token = default(CancellationToken))
        {
            NetworkStream stream = null;
            using (this._resourceAccess.ScopeWait(token))
            {
                CheckClientAvailable();
                stream = this._client.GetStream();
            }

            IPackageDescriptor currentDesc = desc;
            var pkg = new SimplePackage();
            try
            {
                do
                {
                    byte[] bytes = new byte[currentDesc.Size];
                    await stream.ReadAsync(bytes, 0, currentDesc.Size, token);

                    currentDesc.AddDataToStream(bytes, pkg.Stream);

                    currentDesc = currentDesc.HowToReadNext(bytes);


                    token.ThrowIfCancellationRequested();
                }
                while (currentDesc != null);

                pkg.Stream.Seek(0, SeekOrigin.Begin);
            }
            catch (Exception ex)
            {
                pkg.Dispose();

                if (ex is ObjectDisposedException || ex is IOException)
                {
                    this.Connected = ConnectionStatus.Disconnected;
                    throw new ConnectorClosedException("Connection Closed", ex);
                }

                throw ex;
            }

            return pkg;
        }

        /// <summary>
        /// Checks the client available.
        /// UnThread safe
        /// </summary>
        /// <param name="token">The token.</param>
        /// <exception cref="NTLib.Pipe.Network.Exceptions.ConnectorClosedException"></exception>
        protected void CheckClientAvailable()
        {
            if (this._client == null)
                throw new ConnectorClosedException();
        }

        /// <summary>
        /// Checks the validity of the address.
        /// </summary>
        /// <param name="connectionAddr">The connection addr.</param>
        /// <returns>
        ///   <c>True</c> if the address is valid
        /// </returns>
        protected override bool checkAddress(INetworkAddress connectionAddr)
        {
            var uri = connectionAddr.Uri;
            return uri != null &&
                    !string.IsNullOrEmpty(uri.Host) &&
                    uri.Port != 0 &&
                    string.Equals(uri.Scheme, "tcp", System.StringComparison.InvariantCultureIgnoreCase);
        }


        #endregion
    }
}
